//! Generic identifier utils.

use thiserror::Error;

#[derive(Debug, Error)]
pub enum IdentError {
    #[error("invalid chars in identifier")]
    InvalidChars,

    #[error("empty ident string")]
    EmptyIdent,

    #[error("malformed part structure")]
    MalformedParts,
}

/// Identifier string, can be formatted however is necessary for target lang.
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct IdentString(String);

impl IdentString {
    /// Constructs a new instance by combining an iterator of parts.
    pub fn from_parts<S: AsRef<str>>(s: impl IntoIterator<Item = S>) -> Result<Self, IdentError> {
        let mut buf = String::new();

        for part in s {
            if !buf.is_empty() {
                buf.push('_');
            }

            let part_str = part.as_ref();
            check_ident_part(part_str)?;
            buf.push_str(part_str);
        }

        // This does some duplicate checks but it's harmless.
        Self::new_from_snake(buf)
    }

    /// Constructs an identifier by consuming and checking a snake-case string.
    pub fn new_from_snake(s: String) -> Result<Self, IdentError> {
        if s.is_empty() {
            return Err(IdentError::EmptyIdent);
        }

        for p in split_parts(&s) {
            check_ident_part(p)?;
        }

        Ok(Self(s))
    }

    pub fn inner(&self) -> &str {
        &self.0
    }

    pub fn into_inner(self) -> String {
        self.0
    }

    /// Returns an iterator over the parts of the identifier.
    pub fn iter_parts(&self) -> impl Iterator<Item = &str> {
        split_parts(self.inner())
    }

    pub fn as_snake(&self) -> &str {
        self.inner()
    }

    /// Converts to camel case.  Arg specified if it's upper or lower.
    pub fn to_camel(&self, upper: bool) -> String {
        let mut buf = String::new();

        for part in self.iter_parts() {
            let mut char_iter = part.chars();

            // Uppercase just the first letter.
            let Some(first) = char_iter.next() else {
                continue;
            };

            // Conditionally capitalize the first char of a part.
            if upper || !buf.is_empty() {
                buf.push(first.to_ascii_uppercase());
            } else {
                buf.push(first);
            }

            buf.extend(char_iter);
        }

        buf
    }

    /// Converts to Caml_case.
    pub fn to_caml(&self) -> String {
        let mut buf = String::new();

        let mut char_iter = self.0.chars();

        let Some(first) = char_iter.next() else {
            panic!("identifier: empty identifier!");
        };

        // Just capaitalize the first letter of the whole thing.
        buf.push(first.to_ascii_uppercase());
        buf.extend(char_iter);

        buf
    }

    /// Converts the identifier to a plain string according to a specified format.
    pub fn to_format(&self, fmt: IdentFormat) -> String {
        match fmt {
            IdentFormat::Snake => self.as_snake().to_owned(),
            IdentFormat::UpperCamel => self.to_camel(true),
            IdentFormat::LowerCamel => self.to_camel(false),
            IdentFormat::OCaml => self.to_caml(),
        }
    }

    /// Attaches the provided identifier to the end of this one, modifying it in-place.
    pub fn push_suffix(&mut self, suffix: Self) {
        self.0.push('_');
        self.0.push_str(&suffix.0);
    }

    /// Constructs a new identifier by attaching the provided one at the end.
    pub fn chain_suffix(&self, next: Self) -> Self {
        let mut dup = self.clone();
        dup.push_suffix(next);
        dup
    }
}

fn split_parts(s: &str) -> impl Iterator<Item = &str> {
    s.split("_")
}

fn check_ident_part(s: &str) -> Result<(), IdentError> {
    if s.chars().any(|c| !c.is_alphanumeric()) {
        return Err(IdentError::InvalidChars);
    }

    Ok(())
}

/// Identifier formats, for a particular context.
#[derive(Copy, Clone, Debug)]
pub enum IdentFormat {
    /// snake_case
    Snake,

    /// UpperCamelCase
    UpperCamel,

    /// lowerCamelCase
    LowerCamel,

    /// Ocaml_case
    OCaml,
}

/// Collection of naming convention settings for a language.
#[derive(Clone, Debug)]
pub struct LangStyle {
    module_name: IdentFormat,
    module_path: PathSep,
    type_name: IdentFormat,
    function_name: IdentFormat,
    field_name: IdentFormat,
}

macro_rules! gen_ident_type {
    ($name:ident; $style_field:ident) => {
        #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
        pub struct $name(IdentString);

        impl $name {
            pub fn inner(&self) -> &IdentString {
                &self.0
            }

            pub fn into_inner(self) -> IdentString {
                self.0
            }

            pub fn format(&self, f: IdentFormat) -> String {
                self.0.to_format(f)
            }

            pub fn format_lang(&self, style: &LangStyle) -> String {
                self.format(style.$style_field)
            }
        }

        impl From<IdentString> for $name {
            fn from(s: IdentString) -> Self {
                Self(s)
            }
        }

        impl Into<IdentString> for $name {
            fn into(self) -> IdentString {
                self.0
            }
        }
    };
}

gen_ident_type!(ModuleName; module_name);
gen_ident_type!(TypeName; type_name);
gen_ident_type!(FunctionName; function_name);
gen_ident_type!(FieldName; field_name);

/// Path separators, for a context like a module path.
#[derive(Copy, Clone, Debug)]
pub enum PathSep {
    /// Like Python.
    Dot,

    /// Like Rust.
    DoubleColon,

    /// Like C, since they're like prefixes.
    Underscore,
}
